from __future__ import with_statement, division

import pygame

from locals import *
import parse
import objects

class Map(object):
	tileset = {
		".": objects.tiles.Chasm,
		"#": objects.tiles.Wall,
		"_": objects.tiles.Floor,
		
		"~": objects.tiles.Water,
		"@": objects.tiles.Gravel,
		
		"e": objects.tiles.Exit,
		
		"+": objects.tiles.IceSheet,
		"\\": objects.tiles.IceTopRight,
		"/": objects.tiles.IceBottomRight,
		"l": objects.tiles.IceBottomLeft,
		"r": objects.tiles.IceTopLeft,
		
		"o": objects.tiles.ForceRandom,
		"^": objects.tiles.ForceUp,
		">": objects.tiles.ForceRight,
		"v": objects.tiles.ForceDown,
		"<": objects.tiles.ForceLeft,
			
		"?": objects.tiles.RealWall,
		"!": objects.tiles.FakeWall,
		"x": objects.tiles.AppearingWall,
		"&": objects.tiles.Dirt,
		":": objects.tiles.InvisibleWall,
	}

	@classmethod
	def load_map(cls, file):
		map_data = []
		map_objects = []
		map = None
		
		mode = None
		
		name = ""
		author = ""
		
		with file:
			for line in file:
				if line.startswith("\t"):
					line = line.strip()
					
					if line:
						if mode == "details":
							type_name, arguments = line.split(":", 1)
							args = parse.parse(arguments, None)[0]
							
							if type_name == "name":
								name = args
							elif type_name == "author":
								author = args
						elif mode == "map":
							line = line.replace(" ", "")
					
							row = list(line)
							map_data.append(row)
						elif mode == "objects":
							#try:
							object = parse.parse_object(map, line, parse.variables)
							map_objects.append(object)
							#except parse.IncompatibleArguments:
							#print "Incompatible arguments for " + type_name +": " + str(args)
						else:
							raise "Unknown map data: " + mode
				elif line.strip() or (mode != "map" and line.strip().startswith("#")):					
					if mode == "map":
						for y, row in enumerate(map_data):
							for x, cell in enumerate(row):
								row[x] = Map.tileset[cell.lower()](map, (x, y))
						
						map = cls(Map.tileset, map_data, [])#map_objects)
						
						for y, row in enumerate(map_data):
							for x, cell in enumerate(row):
								row[x].map = map
						
						map.name = name
						map.author = author
					
					mode = line.strip().lower()
		
		for object in map_objects:
			map.insert_object(object)
			
		map._order_objects()
		#map._index_objects()
		
		return map
		
	@staticmethod
	def save_map(map, file):
		tile_lookup = {}
		for key, value in map.tileset.iteritems():
			tile_lookup[value] = key
			
		tile_lookup = dict(zip(map.tileset.itervalues(), map.tileset.iterkeys()))
		
		with file:
			file.writelines(("details\n","\tname: \"%s\"\n" % map.name, "\tauthor: \"%s\"\n" % map.author))
			
			file.writelines(("map\n"))
		
			for row in map.map_data:
				row = "".join(tile.symbol() for tile in row)
				line = "\t%s\n" % row
				
				file.writelines((line))
				
			file.writelines(("objects\n"))
			
			file.writelines("\t%s\n" % object.save(parse.variables) for object in map.objects)

	def __init__(self, tileset, map_data, objects):
		self.tileset = tileset
		
		self.map_data = map_data
		self.objects = objects
		
		width, height = len(self[0]), len(self)
		self.size = (width, height)
		
		self.object_index = [[set() for x in xrange(width)] for y in xrange(height)]
		
		self.tile_render = pygame.Surface((width * 32, height * 32)).convert()
		self._render_tiles(self.tile_render)
		
		self.object_render = pygame.Surface((width * 32, height * 32)).convert_alpha()
		self._order_objects()
		#self._render_objects(self.object_render, 0)
		
		self.completed = False
	
	def __getitem__(self, index): return self.map_data[index]
	def __len__(self): return len(self.map_data)
		
	def _configuration(self, position):
		width, height = self.size
		x, y = position

		x = clamp(0, x, width - 1)
		y = clamp(0, y, height - 1)
		
		subtile = 0
		configuration = []
		cell = self.map_data[y][x]
					
		configuration = [
			[False, False, False],
			[False, False, False],
			[False, False, False]]
					
		for off_y in xrange(-1, 2):
			for off_x in xrange(-1, 2):
				if 0 <= x + off_x < width:
					if 0 <= y + off_y < height:
						value = (self[y + off_y][x + off_x].type == cell.type)
					else:
						value = True
				else:
					value = True
				
				configuration[off_y + 1][off_x + 1] = value
		
		return configuration
	
	def _render_tiles(self, surface, rect=None):
		if not rect:
			width, height = self.size
			rect = (0, 0, width, height)
		
		surface.fill((0, 0, 0), tuple(max(0, coord * 32) for coord in rect))
		rect_x, rect_y, rect_width, rect_height = rect
		
		for y, row in enumerate(self):
			for x, cell in enumerate(row):	
				if rect_x <= x <= rect_x + rect_width and rect_y <= y <= rect_y + rect_height:
					configuration = self._configuration((x, y))
					
					cell.draw(surface, configuration)
	
	def insert_object(self, object):
		x, y = object.position
		
		self.objects.append(object)
		self.object_index[y][x].add(object)
	
	def remove_object(self, object):
		x, y = object.position
		
		self.objects.remove(object)
		self.object_index[y][x].remove(object)
	
	def move_object(self, object):
		new_x, new_y = object.position
		old_x, old_y = object.last_position
		
		self.object_index[old_y][old_x].remove(object)
		self.object_index[new_y][new_x].add(object)
	
	def _order_objects(self):
		def z_index(object):
			return objects.Z_ORDER.index(object.__class__)
		
		self.objects.sort(key=z_index, reverse=True)
	
	def _render_objects(self, surface, viewport=None):
		if not viewport:
			width, height = self.size
			viewport = (0, 0, width * 32, height * 32)
		
		surface.fill((0, 0, 0, 0), viewport)
	
		x, y, width, height = viewport
	
		for object in self.objects:
			object_x, object_y = object.position
			
			if x - 32 <= object_x * 32 <= x + width + 32:
				if y - 32 <= object_y * 32 <= y + height + 32:
					object.draw(surface)
	
	def update(self):
		for object in self.objects:
			object.update()

	def draw(self, surface, viewport):
		self._render_objects(self.object_render, viewport)
		surface.blit(self.tile_render, (0, 0), viewport)
		surface.blit(self.object_render, (0, 0), viewport)
	
	def objects_at_pos(self, position, exclude=[]):
		x, y = position
		index = self.object_index
		
		return [object for object in index[y][x] if object not in exclude]
	
	def passable(self, position, passer):
		x, y = position
		
		if self[y][x].passable(passer):
			passable = True
			
			for object in self.objects_at_pos(position, (passer,)):
				if not object.passable(passer):
					passable = False
					break
			
			return passable
		else:
			return False
			
	def push(self, position, pusher, movement, speed):
		x, y = position
		
		if self[y][x].passable(pusher):
			results = []
			
			for object in self.objects_at_pos(position, (pusher,)):
				results.append(object.push(pusher, movement, speed))
			
			return all(results)
		else:
			return self[y][x].push(pusher, movement, speed)
			
	def step(self, position, stepper):
		x, y = position
		
		self[y][x].step(stepper)
		
		for object in self.objects_at_pos(position, (stepper,)):
			object.step(stepper)

	def stand(self, position, stander):
		x, y = position
		
		stander.frozen = (False, False)
		
		self[y][x].stand(stander)
		
		for object in self.objects_at_pos(position, (stander,)):
			object.stand(stander)
			
	def exit(self):
		self.completed = True
		
	def toggle_walls(self):
		for object in self.objects:
			if object.__class__.__name__ == "ToggleWall":
				object.open = not object.open
				
	def reverse_tanks(self):
		for object in self.objects:
			if object.__class__.__name__ == "Tank":
				object.reverse()
